package com.zfdang.touchhelper.tool;

import android.accessibilityservice.AccessibilityService;
import android.accessibilityservice.GestureDescription;
import android.annotation.SuppressLint;
import android.app.Service;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.graphics.Path;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.inputmethod.InputMethodInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;

import com.aivin.adovertool.BuildConfig;
import com.aivin.adovertool.R;
import com.zfdang.touchhelper.bean.PackagePositionInfoBean;
import com.zfdang.touchhelper.bean.PackageWidgetInfoBean;
import com.zfdang.touchhelper.broadcast.PackageChangeReceiver;
import com.zfdang.touchhelper.broadcast.UserPresentReceiver;
import com.zfdang.touchhelper.callback.IwindowResultCallback;
import com.zfdang.touchhelper.config.BaseConfig;
import com.zfdang.touchhelper.config.LyActionTypeConfig;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class LyAccessibilityServiceTool {

    private static final String TAG = "Touc hHelperServiceImpl";
    private static final String SelfPackageName = "开屏跳过";
    private final AccessibilityService accessibilityService;


    private PackageChangeReceiver packageChangeReceiver;
    private UserPresentReceiver userPresentReceiver;

    public Handler receiverHandler;

    private ScheduledExecutorService taskExecutorService;

    private volatile boolean skipAdRunning, skipAdByActivityPosition, skipAdByActivityWidget, skipAdByKeyword;
    //private PackageManager packageManager;
    private String currentPackageName, currentActivityName;
    private String packageName;
    private Set<String> setPackages, setIMEApps, setWhiteList;
    private Set<String> clickedWidgets;
    private List<String> keyWordList;

    private Map<String, PackagePositionInfoBean> mapPackagePositions;
    private Map<String, Set<PackageWidgetInfoBean>> mapPackageWidgets;
    private volatile Set<PackageWidgetInfoBean> setTargetedWidgets;

    // try to click 5 times, first click after 300ms, and delayed for 500ms for future clicks
    private static final int PACKAGE_POSITION_CLICK_FIRST_DELAY = 300;
    private static final int PACKAGE_POSITION_CLICK_RETRY_INTERVAL = 500;
    private static final int PACKAGE_POSITION_CLICK_RETRY = 6;
    private boolean isShow = false;

    public LyAccessibilityServiceTool(AccessibilityService accessibilityService) {
        this.accessibilityService = accessibilityService;
        lyWindowTool = new LyWindowTool(accessibilityService,callback ,accessibilityService);
    }

    public void onServiceConnected() {
        currentPackageName = "Initial PackageName";
        currentActivityName = "Initial ClassName";
        packageName = accessibilityService.getPackageName();

        clickedWidgets = new HashSet<>();
        taskExecutorService = Executors.newSingleThreadScheduledExecutor();

        // 从缓存中读取最新配置
        keyWordList = SettingsTool.getInstance().getKeyWordList();
        setWhiteList = SettingsTool.getInstance().getWhitelistPackages();
        mapPackageWidgets = SettingsTool.getInstance().getPackageWidgets();
        mapPackagePositions = SettingsTool.getInstance().getPackagePositions();

        initHandler();
        updatePackage();
        registerBroadcastReceiver(accessibilityService);
    }

    public void onInterrupt() {
        stopSkipAdProcess();
    }

    private void initHandler() {
        receiverHandler = new Handler(Looper.getMainLooper(), msg -> {
            switch (msg.what) {
                case LyActionTypeConfig.ACTION_REFRESH_KEYWORDS:
                    keyWordList = SettingsTool.getInstance().getKeyWordList();
                    break;

                case LyActionTypeConfig.ACTION_REFRESH_PACKAGE:
                    setWhiteList = SettingsTool.getInstance().getWhitelistPackages();
                    updatePackage();
                    break;

                case LyActionTypeConfig.ACTION_REFRESH_CUSTOMIZED_ACTIVITY:
                    mapPackageWidgets = SettingsTool.getInstance().getPackageWidgets();
                    mapPackagePositions = SettingsTool.getInstance().getPackagePositions();
                    break;

                case LyActionTypeConfig.ACTION_STOP_SERVICE:
                    accessibilityService.disableSelf();
                    break;

                case LyActionTypeConfig.ACTION_ACTIVITY_CUSTOMIZATION:
                    showActivityCustomizationDialog();
                    break;

                case LyActionTypeConfig.ACTION_START_SKIPAD:
                    startSkipAdProcess();
                    break;

                case LyActionTypeConfig.ACTION_STOP_SKIPAD:
                    stopSkipAdProcessInner();
                    break;
            }
            return true;
        });
    }

    private void registerBroadcastReceiver(Service service) {
        userPresentReceiver = new UserPresentReceiver();
        service.registerReceiver(userPresentReceiver, new IntentFilter(Intent.ACTION_USER_PRESENT));

        packageChangeReceiver = new PackageChangeReceiver();
        IntentFilter actions = new IntentFilter();
        actions.addAction(Intent.ACTION_PACKAGE_ADDED);
        actions.addAction(Intent.ACTION_PACKAGE_REMOVED);
        service.registerReceiver(packageChangeReceiver, actions);
    }


    public void onAccessibilityEvent(AccessibilityEvent event) {
        if (BuildConfig.DEBUG) {
            Log.d(TAG, AccessibilityEvent.eventTypeToString(event.getEventType()) + " - " + event.getPackageName() + " - " + event.getClassName() + "; ");
            Log.d(TAG, "    currentPackageName = " + currentPackageName + "  currentActivityName = " + currentActivityName);
        }
        CharSequence tempPkgName = event.getPackageName();
        CharSequence tempClassName = event.getClassName();
        if (tempPkgName == null || tempClassName == null) {
            return;
        }
        try {
            switch (event.getEventType()) {
                case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED:
                    String pkgName = tempPkgName.toString();
                    if (setIMEApps.contains(pkgName)) {
                        // IME might be temporarily started in the package, skip this event
                        break;
                    }
                    final String actName = tempClassName.toString();
                    boolean isActivity = !actName.startsWith("android.") && !actName.startsWith("androidx.");
                    if (!currentPackageName.equals(pkgName)) {
                        // new package, is it a activity?
                        if (isActivity) {
                            // yes, it's an activity
                            // since it's an activity in another package, it must be a new activity, save them
                            currentPackageName = pkgName;
                            currentActivityName = actName;
                            // stop current skip ad process if it exists
                            stopSkipAdProcess();
                            if (setPackages.contains(pkgName)) {
                                // if the package is in our list, start skip ads process
                                // this is the only place to start skip ad process
                                startSkipAdProcess();
                            }
                        }
                    } else {
                        // current package, we just save the activity
                        if (isActivity) {
                            // yes, it's an activity
                            if (!currentActivityName.equals(actName)) {
                                currentActivityName = actName;
                                break;
                            }
                        }
                    }

                    // first method is to skip ads by position in activity
                    if (skipAdByActivityPosition) {
                        // run this method for once only
                        skipAdByActivityPosition = false;

                        PackagePositionInfoBean packagePositionInfoBean = mapPackagePositions.get(currentPackageName);
                        if (packagePositionInfoBean != null) {
                            ShowToastInIntentService("正在根据位置跳过广告...");

                            // try to click the position in the activity for multiple times
                            final Future<?>[] futures = {null};
                            futures[0] = taskExecutorService.scheduleAtFixedRate(new Runnable() {
                                int num = 0;

                                @Override
                                public void run() {
                                    if (num < PACKAGE_POSITION_CLICK_RETRY) {
                                        if (currentActivityName.equals(packagePositionInfoBean.activityName)) {
                                            // current activity is null, or current activity is the target activity
                                            if (BuildConfig.DEBUG) {
                                                Log.d(TAG, "Find skip-ad by position, simulate click now! ");
                                            }
                                            click(packagePositionInfoBean.x, packagePositionInfoBean.y, 0, 40);
                                        }
                                        num++;
                                    } else {
                                        futures[0].cancel(true);
                                    }
                                }
                            }, PACKAGE_POSITION_CLICK_FIRST_DELAY, PACKAGE_POSITION_CLICK_RETRY_INTERVAL, TimeUnit.MILLISECONDS);
                        }
                    }

                    // second: skip ads by widget
                    if (skipAdByActivityWidget) {
                        if (BuildConfig.DEBUG) {
                            Log.d(TAG, "method by widget in STATE_CHANGED");
                        }
                        // run this once, and find targeted widgets for this package
                        skipAdByActivityWidget = false;
                        setTargetedWidgets = mapPackageWidgets.get(currentPackageName);
                    }
                    if (setTargetedWidgets != null) {
                        if (BuildConfig.DEBUG) {
                            Log.d(TAG, "Find skip-ad by widget, simulate click ");
                        }
                        // this code could be run multiple times
                        final AccessibilityNodeInfo node = accessibilityService.getRootInActiveWindow();
                        final Set<PackageWidgetInfoBean> widgets = setTargetedWidgets;
                        taskExecutorService.execute(() -> iterateNodesToSkipAd(node, widgets));
                    }

                    if (skipAdByKeyword) {
                        if (BuildConfig.DEBUG) {
                            Log.d(TAG, "method by keywords in STATE_CHANGED");
                        }
                        // this code could be run multiple times
                        final AccessibilityNodeInfo node = accessibilityService.getRootInActiveWindow();
                        taskExecutorService.execute(() -> iterateNodesToSkipAd(node, null));
                    }
                    break;
                case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED:
                    if (!setPackages.contains(tempPkgName.toString())) {
                        break;
                    }

                    if (setTargetedWidgets != null) {
                        if (BuildConfig.DEBUG) {
                            Log.d(TAG, "method by widget in CONTENT_CHANGED");
                        }
                        final AccessibilityNodeInfo node = event.getSource();
                        final Set<PackageWidgetInfoBean> widgets = setTargetedWidgets;
                        taskExecutorService.execute(() -> iterateNodesToSkipAd(node, widgets));
                    }

                    if (skipAdByKeyword) {
                        if (BuildConfig.DEBUG) {
                            Log.d(TAG, "method by keywords in CONTENT_CHANGED");
                        }
                        final AccessibilityNodeInfo node = event.getSource();
                        taskExecutorService.execute(() -> iterateNodesToSkipAd(node, null));
                    }
                    break;
            }
        } catch (Throwable e) {
            LyLog.showTraceStackInString(TAG, e);
        }
    }

    public void onUnbind(Intent intent) {
        try {
            accessibilityService.unregisterReceiver(userPresentReceiver);
            accessibilityService.unregisterReceiver(packageChangeReceiver);
        } catch (Throwable e) {
            LyLog.showTraceStackInString(TAG, e);
        }
    }

    /**
     * 遍历节点跳过广告
     *
     * @param root 根节点
     * @param set  传入set时按控件判断，否则按关键词判断
     */
    private void iterateNodesToSkipAd(AccessibilityNodeInfo root, Set<PackageWidgetInfoBean> set) {
        ArrayList<AccessibilityNodeInfo> topNodes = new ArrayList<>();
        topNodes.add(root);
        ArrayList<AccessibilityNodeInfo> childNodes = new ArrayList<>();

        int total = topNodes.size();
        int index = 0;
        AccessibilityNodeInfo node;
        boolean handled;
        while (index < total && skipAdRunning) {
            node = topNodes.get(index++);
            if (node != null) {
                if (set != null) {
                    handled = skipAdByTargetedWidget(node, set);
                } else {
                    handled = skipAdByKeywords(node);
                }
                if (handled) {
                    node.recycle();
                    break;
                }
                for (int n = 0; n < node.getChildCount(); n++) {
                    childNodes.add(node.getChild(n));
                }
                node.recycle();
            }
            if (index == total) {
                // topNodes ends, now iterate child nodes
                topNodes.clear();
                topNodes.addAll(childNodes);
                childNodes.clear();
                index = 0;
                total = topNodes.size();
            }
        }
        // ensure unprocessed nodes get recycled
        while (index < total) {
            node = topNodes.get(index++);
            if (node != null) {
                node.recycle();
            }
        }
        index = 0;
        total = childNodes.size();
        while (index < total) {
            node = childNodes.get(index++);
            if (node != null) {
                node.recycle();
            }
        }
    }

    /**
     * 查找并点击包含keyword控件，目标包括Text和Description
     */
    private boolean skipAdByKeywords(AccessibilityNodeInfo node) {
        if (BuildConfig.DEBUG) {
            Log.d(TAG, "skipAdByKeywords triggered: " + LyAccessibilityTool.getDescribeOfAccessibilityNode(node));
        }
        CharSequence description = node.getContentDescription();
        CharSequence text = node.getText();
        if (TextUtils.isEmpty(description) && TextUtils.isEmpty(text)) {
            return false;
        }
        // try to find keyword
        boolean isFound = false;
        for (String keyword : keyWordList) {
            // text or description contains keyword, but not too long （<= length + 6）
            if (text != null && (text.toString().length() <= keyword.length() + 6) && text.toString().contains(keyword) && !text.toString().equals(SelfPackageName)) {
                isFound = true;
            } else if (description != null && (description.toString().length() <= keyword.length() + 6) && description.toString().contains(keyword) && !description.toString().equals(SelfPackageName)) {
                isFound = true;
            }
            if (isFound) {
                // if this node matches our target, stop finding more keywords
                if (BuildConfig.DEBUG) {
                    Log.d(TAG, "identify keyword = " + keyword);
                }
                break;
            }
        }
        // if this node matches our target, try to click it
        if (isFound) {
            String nodeDesc = LyAccessibilityTool.getDescribeOfAccessibilityNode(node);
            if (BuildConfig.DEBUG) {
                Log.d(TAG, nodeDesc);
            }
            if (!clickedWidgets.contains(nodeDesc)) {
                clickedWidgets.add(nodeDesc);

                ShowToastInIntentService("正在根据关键字跳过广告...");
                boolean clicked = node.performAction(AccessibilityNodeInfo.ACTION_CLICK);
                if (BuildConfig.DEBUG) {
                    Log.d(TAG, "self clicked = " + clicked);
                }
                if (!clicked) {
                    Rect rect = new Rect();
                    node.getBoundsInScreen(rect);
                    click(rect.centerX(), rect.centerY(), 0, 20);
                }

                // is it possible that there are more nodes to click and this node does not work?
                return true;
            }
        }
        return false;
    }

    /**
     * 查找并点击由 ActivityWidgetDescription 定义的控件
     */
    private boolean skipAdByTargetedWidget(AccessibilityNodeInfo node, Set<PackageWidgetInfoBean> set) {
        Rect temRect = new Rect();
        node.getBoundsInScreen(temRect);
        CharSequence cId = node.getViewIdResourceName();
        CharSequence cDescribe = node.getContentDescription();
        CharSequence cText = node.getText();
        for (PackageWidgetInfoBean e : set) {
            boolean isFound = false;
            if (temRect.equals(e.position)) {
                isFound = true;
            } else if (cId != null && !e.idName.isEmpty() && cId.toString().equals(e.idName)) {
                isFound = true;
            } else if (cDescribe != null && !e.description.isEmpty() && cDescribe.toString().contains(e.description)) {
                isFound = true;
            } else if (cText != null && !e.text.isEmpty() && cText.toString().contains(e.text)) {
                isFound = true;
            }

            if (isFound) {
                if (BuildConfig.DEBUG) {
                    Log.d(TAG, "Find skip-ad by Widget " + e.toString());
                }
                String nodeDesc = LyAccessibilityTool.getDescribeOfAccessibilityNode(node);
                if (!clickedWidgets.contains(nodeDesc)) {
                    // add this widget to clicked widget, avoid multiple click on the same widget
                    clickedWidgets.add(nodeDesc);

                    ShowToastInIntentService("正在根据控件跳过广告...");
                    if (e.onlyClick) {
                        click(temRect.centerX(), temRect.centerY(), 0, 20);
                    } else {
                        if (!node.performAction(AccessibilityNodeInfo.ACTION_CLICK)) {
                            if (!node.getParent().performAction(AccessibilityNodeInfo.ACTION_CLICK)) {
                                click(temRect.centerX(), temRect.centerY(), 0, 20);
                            }
                        }
                    }
                    // clear setWidgets, stop trying
                    if (setTargetedWidgets == set) {
                        setTargetedWidgets = null;
                    }
                    return true;
                }
            }
        }
        return false;
    }

    private void showAllChildren(AccessibilityNodeInfo root) {
        ArrayList<AccessibilityNodeInfo> roots = new ArrayList<>();
        roots.add(root);
        ArrayList<AccessibilityNodeInfo> nodeList = new ArrayList<>();
        findAllNode(roots, nodeList, "");
    }

    /**
     * 查找所有的控件
     */
    private void findAllNode(List<AccessibilityNodeInfo> roots, List<AccessibilityNodeInfo> list, String indent) {
        ArrayList<AccessibilityNodeInfo> childrenList = new ArrayList<>();
        for (AccessibilityNodeInfo e : roots) {
            if (e == null) {
                continue;
            }
            list.add(e);
            if (BuildConfig.DEBUG) {
                Log.d(TAG, indent + LyAccessibilityTool.getDescribeOfAccessibilityNode(e));
            }
            for (int n = 0; n < e.getChildCount(); n++) {
                childrenList.add(e.getChild(n));
            }
        }
        if (!childrenList.isEmpty()) {
            findAllNode(childrenList, list, indent + "  ");
        }
    }

    private String dumpRootNode(AccessibilityNodeInfo root) {
        ArrayList<AccessibilityNodeInfo> nodeList = new ArrayList<>();
        StringBuilder dumpString = new StringBuilder();
        dumpChildNodes(root, nodeList, dumpString, "");
        return dumpString.toString();
    }

    private void dumpChildNodes(AccessibilityNodeInfo root, List<AccessibilityNodeInfo> list, StringBuilder dumpString, String indent) {
        if (root == null) {
            return;
        }
        list.add(root);
        dumpString.append(indent + LyAccessibilityTool.getDescribeOfAccessibilityNode(root) + "\n");

        for (int n = 0; n < root.getChildCount(); n++) {
            AccessibilityNodeInfo child = root.getChild(n);
            dumpChildNodes(child, list, dumpString, indent + " ");
        }
    }

    /**
     * 模拟点击
     */
    private boolean click(int X, int Y, long start_time, long duration) {
        Path path = new Path();
        path.moveTo(X, Y);
        GestureDescription.Builder builder = new GestureDescription.Builder()
                .addStroke(new GestureDescription.StrokeDescription(path, start_time, duration));
        return accessibilityService.dispatchGesture(builder.build(), null, null);
    }

    /**
     * start the skip-ad process
     */
    private void startSkipAdProcess() {
        if (BuildConfig.DEBUG) {
            Log.d(TAG, "Start Skip-ad process");
        }
        skipAdRunning = true;
        skipAdByActivityPosition = true;
        skipAdByActivityWidget = true;
        skipAdByKeyword = true;
        setTargetedWidgets = null;
        clickedWidgets.clear();

        // cancel all methods N seconds later
        receiverHandler.removeMessages(LyActionTypeConfig.ACTION_STOP_SKIPAD);

        int time = LyPreferenceTool.getInstance().getInt(BaseConfig.SKIP_AD_DURATION, 4);
        receiverHandler.sendEmptyMessageDelayed(LyActionTypeConfig.ACTION_STOP_SKIPAD, time * 1000L);
    }

    /**
     * stop the skip-ad process
     */
    private void stopSkipAdProcess() {
        if (BuildConfig.DEBUG) {
            Log.d(TAG, "Stop Skip-ad process");
        }
        stopSkipAdProcessInner();
        receiverHandler.removeMessages(LyActionTypeConfig.ACTION_STOP_SKIPAD);
    }

    /**
     * stop the skip-ad process, without cancel scheduled task
     */
    private void stopSkipAdProcessInner() {
        skipAdRunning = false;
        skipAdByActivityPosition = false;
        skipAdByActivityWidget = false;
        skipAdByKeyword = false;
        setTargetedWidgets = null;
    }

    /**
     * find all packages while launched. also triggered when receive package add / remove events
     */
    private void updatePackage() {
        PackageManager packageManager = accessibilityService.getPackageManager();

        setPackages = new HashSet<>();
        setIMEApps = new HashSet<>();
        Set<String> setTemps = new HashSet<>();

        // find all launchers
        Intent intent = new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_LAUNCHER);
        List<ResolveInfo> ResolveInfoList = packageManager.queryIntentActivities(intent, PackageManager.MATCH_ALL);
        for (ResolveInfo e : ResolveInfoList) {
            setPackages.add(e.activityInfo.packageName);
        }
        // find all homes
        intent = new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME);
        ResolveInfoList = packageManager.queryIntentActivities(intent, PackageManager.MATCH_ALL);
        for (ResolveInfo e : ResolveInfoList) {
            setTemps.add(e.activityInfo.packageName);
        }
        // find all input methods
        List<InputMethodInfo> inputMethodInfoList = ((InputMethodManager) accessibilityService.getSystemService(AccessibilityService.INPUT_METHOD_SERVICE)).getInputMethodList();
        for (InputMethodInfo e : inputMethodInfoList) {
            setIMEApps.add(e.getPackageName());
        }

        // ignore some packages in hardcoded way
        // https://support.google.com/a/answer/7292363?hl=en
        setTemps.add(this.packageName);
        setTemps.add("com.android.settings");

        // remove whitelist, systems, homes & ad-hoc packages from pkgLaunchers
        setPackages.removeAll(setWhiteList);
        setPackages.removeAll(setIMEApps);
        setPackages.removeAll(setTemps);

    }

    private final IwindowResultCallback callback = new IwindowResultCallback() {
        @Override
        public void onWindowShow(boolean isShowNow) {
            isShow = isShowNow;
        }

        @Override
        public void needFindAllNode(List<AccessibilityNodeInfo> roots, List<AccessibilityNodeInfo> nodeList, String indent) {
            findAllNode(roots, nodeList, indent);
        }

        @Override
        public String needDumpRootNode(AccessibilityNodeInfo root) {
            return dumpRootNode(root);
        }

        @Override
        public void needToastMsg(String msg) {
            ShowToastInIntentService(msg);
        }
    };


    private final LyWindowTool lyWindowTool  ;
    /**
     * 手动添加跳过的按钮或者坐标
     */
    private void showActivityCustomizationDialog() {
        if (isShow) {
            return;
        }
        lyWindowTool.showActivityCustomizationDialog(
                currentPackageName,
                currentActivityName,
                mapPackagePositions,
                mapPackageWidgets );

    }

    public void ShowToastInIntentService(final String sText) {

        boolean needNotification = LyPreferenceTool.getInstance().getBoolean(BaseConfig.SKIP_AD_NOTIFICATION, true);
        if (needNotification) {
            receiverHandler.post(() -> {
                Toast toast = Toast.makeText(accessibilityService, sText, Toast.LENGTH_SHORT);
                toast.show();
            });
        }
    }
}
